/**
 *
 * 18所静态分析报告
 *
 * Created by nongzhizhong on 2025/04/23
 *
 */
const path = require('path');
const compressing = require('compressing');
const mongoose = require('mongoose');
const fs = require('fs-extra');
const { spawn } = require('child_process');
const files = require('../../../files');
const functionRelations = require('../../../function-relations');
const invokeRelations = require('../../../invoke-relations');
const FileDataFlow = require('../../../mongo/data-flow');
const BaseReporter = require('../base-report');
const utility = require('../../../utility');
const functions = require('../../../functions');
const fileElements = require('../../../file-elements');
const defects = require('../../../defects');
const Excels = require('../adapter/excel');
const logger = require('../../../logger');
const ProgressIndicator = require('../../../progress-indicator');

/**
  DD（两次变量定义之间没有被使用），DU（变量定义但没有使用），UR（变量未定义就引用）
  RO(只引用) DO(只定义) DR(既定义也使用)
  R(引用) D(定义) E(声明) I(作为输入) O(作为输出)
 */
const variableCheck = {
  unusedBetweenDefs: 'DD',
  definedUnused: 'DU',
  undefinedRef: 'UR',
  refOnly: 'RO',
  defOnly: 'DO',
  defAndUsed: 'DR',
  reference: 'R',
  definition: 'D',
  declaration: 'E',
  input: 'I',
  output: 'O',
};
class DefaultReporter extends BaseReporter {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format, filters = {}) {
    super(versionId, locale, version, project, creator, outputDir, __dirname, format);
    this.reportData = {
      UR: [],
      DU: [],
      DD: [],
      files: [],
      rules: [],
      userName: creator.username,
      defectCount: 0,
      maxCycleComplexity: 0,
      maxFanOut: 0,
      maxInvokeFuncDepth: 0,
      funcCount: 0,
      maxFuncCodeLine: 0,
      minFuncCodeLine: 0,
      cycleComplexityGreaterThan10: [],
      invokeDepthGreaterThan6: [],
      fanOutAndFanInEqualZero: [],
      fanOutGreaterThan7: [],
      invokedFlowImage: '',
      invokedFlowData: {
        nodes: new Map(),
        edges: new Set(),
      },
    };
    this.funcRelationsOfVersions = [];
    this.sourceFileSuffixes = [];
    this.filters = filters;
  }

  async generate(progressIndicator = new ProgressIndicator()) {
    const result = {
      cachePath: this.outputDir,
    };
    this.handleFileSuffixes();
    const [filesProgressIndicator,
      generateImageProgressIndictor,
      staticAnalyzeProgressIndictor,
      generateReportProgressIndictor] = progressIndicator.split([0.5, 0.1, 0.2, 0.2]);
    this.funcRelationsOfVersions = await functionRelations.getRelationsByVersion(this.versionId);
    this.versionFuncsMap = await functions.getFunctionByVersionId(this.versionId, ['_id', 'functionName', 'fanIn']);
    const { versionDir } = await this.spliteByFile(null, filesProgressIndicator);
    await this.generateFuncsFlowChart();
    generateImageProgressIndictor.finish();
    await this.handleStaticAnalyze(path.join(versionDir, '静态分析编码规则集（公开）.xlsx'));
    staticAnalyzeProgressIndictor.finish();
    await this.generateSummaryReport(this.reportData, path.join(versionDir, '静态分析报告（公开）'));
    const fileName = `${this.project.projectName}-${this.version.versionName}-${this.getReportName()}-${this.format}.zip`;
    result.reportPath = path.join(this.outputDir, fileName);
    const reportsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(reportsDir, result.reportPath);
    generateReportProgressIndictor.finish();
    return result;
  }

  /**
     * 计算百分比
     * @param {Number} numerator 分子
     * @param {Number} denominator 分母
     * @returns
     */
  handlePercent(numerator, denominator) {
    if (denominator === 0) {
      throw new Error('Denominator cannot equal to 0');
    }
    if (numerator === 0) {
      return '0%';
    }
    let precent = (numerator / denominator) * 100;
    if (precent.toString().indexOf('.') !== -1 && precent.toString().split('.')[1].length > 2) {
      precent = precent.toFixed(2);
    }
    return `${precent}%`;
  }

  handleFileSuffixes() {
    Object.keys(this.version.languages).forEach((language) => {
      if (!this.version.languages[language].suffixes) {
        return;
      }
      this.sourceFileSuffixes.push(...this.version.languages[language].suffixes);
    });
  }

  /**
   * 处理单个文件数据流信息
   * @param {Object} file 单个文件信息
   * @returns {Promise}
   *
   check:
          DD（两次变量定义之间没有被使用），DU（变量定义但没有使用），UR（变量未定义就引用）
          RO(只引用) DO(只定义) DR(既定义也使用)
          R(引用) D(定义) E(声明) I(作为输入) O(作为输出)
   kind:
        L(局部变量) G(全局变量) P(参数) LG(局部全局变量/局部静态变量)
   *
   */
  async handleFileDataStream(file) {
    const FileDataFlows = await FileDataFlow.find({
      fileId: file._id,
      versionId: mongoose.Types.ObjectId(this.versionId),
    }, ['check', 'var', 'range', 'kind', 'function']).lean();
    const urMap = new Map();
    const duMap = new Map();
    const ddMap = new Map();
    FileDataFlows.forEach((dataFlow) => {
      const {
        check, var: variableName, range, kind,
      } = dataFlow;
      if (!range || !kind || !check) {
        return;
      }
      const beginLine = range.begin && range.begin.line ? range.begin.line : '';
      const endLine = range.end && range.end.line ? range.end.line : '';
      if (!beginLine || !endLine) {
        return;
      }
      switch (check) {
        case variableCheck.undefinedRef: {
          if (!urMap.has(variableName)) {
            urMap.set(variableName, {
              variableName,
              declarationLocation: [],
              referenceLocation: [],
            });
          }
          const obj = urMap.get(variableName);
          obj.declarationLocation.push(beginLine);
          obj.referenceLocation.push(endLine);
          break;
        }
        case variableCheck.definedUnused: {
          if (!duMap.has(variableName)) {
            duMap.set(variableName, {
              variableName,
              definitionLocation: [],
              endLocation: [],
            });
          }
          const obj = duMap.get(variableName);
          obj.definitionLocation.push(beginLine);
          obj.endLocation.push(endLine);
          break;
        }
        case variableCheck.unusedBetweenDefs: {
          if (!ddMap.has(variableName)) {
            ddMap.set(variableName, {
              variableName,
              definitionLocation: [],
              redefinitiontLocation: [],
            });
          }
          const obj = ddMap.get(variableName);
          obj.definitionLocation.push(beginLine);
          if (beginLine !== endLine) {
            obj.redefinitiontLocation.push(endLine);
          }
          break;
        }
        default: break;
      }
    });
    [
      { key: 'UR', value: urMap },
      { key: 'DU', value: duMap },
      { key: 'DD', value: ddMap },
    ].forEach(({ key, value }) => {
      value.forEach((childValue) => {
        if (childValue.declarationLocation) {
          childValue.declarationLocation = this.arraySortAndDeduplication(childValue.declarationLocation).toString();
        }
        if (childValue.referenceLocation) {
          childValue.referenceLocation = this.arraySortAndDeduplication(childValue.referenceLocation).toString();
        }
        if (childValue.definitionLocation) {
          childValue.definitionLocation = this.arraySortAndDeduplication(childValue.definitionLocation).toString();
        }
        if (childValue.endLocation) {
          childValue.endLocation = this.arraySortAndDeduplication(childValue.endLocation).toString();
        }
        if (childValue.redefinitiontLocation) {
          childValue.redefinitiontLocation = this.arraySortAndDeduplication(childValue.redefinitiontLocation).toString();
        }
        this.reportData[key].push(childValue);
      });
    });
  }

  async handleFunctions(file) {
    const funcs = await functions.getFunctions(this.versionId, { kinds: fileElements.getCountableKinds() }, file._id);
    if (funcs.length === 0) {
      return;
    }
    await utility.arrayChunkOperation(funcs, 30, async (func) => {
      this.reportData.funcCount += 1;
      const funcCodeLine = func.endLocation.line - func.location.line;
      this.reportData.maxFanOut = Math.max(this.reportData.maxFanOut, func.fanOut);
      this.reportData.maxFuncCodeLine = Math.max(this.reportData.maxFuncCodeLine, funcCodeLine);
      if (this.reportData.minFuncCodeLine === 0) {
        this.reportData.minFuncCodeLine = funcCodeLine;
      } else {
        this.reportData.minFuncCodeLine = Math.min(this.reportData.minFuncCodeLine, funcCodeLine);
      }
      this.reportData.maxCycleComplexity = Math.max(this.reportData.maxCycleComplexity, func.cycleComplexity);
      if (func.fanOut > 7) {
        this.reportData.fanOutGreaterThan7.push({
          funcName: func.functionName,
          fanOut: func.fanOut,
        });
      }
      if (func.cycleComplexity > 10) {
        this.reportData.cycleComplexityGreaterThan10.push({
          funcName: func.functionName,
          cycleComplexity: func.cycleComplexity,
        });
      }
      if (func.fanIn === func.fanOut && func.fanIn === 0) {
        this.reportData.fanOutAndFanInEqualZero.push({
          funcName: func.functionName,
          filePath: file.path,
        });
      }
      if (!this.reportData.invokedFlowData.nodes.has(func.functionName)) {
        const node = {
          funcName: func.functionName,
          type: 'defined',
          fanIn: func.fanIn,
        };
        this.reportData.invokedFlowData.nodes.set(func.functionName, node);
      }
      let relations = await functionRelations.retrieveRelations(
        func._id,
        this.versionId,
        this.funcRelationsOfVersions
      );
      relations = await invokeRelations.filterRelations(func._id, relations, { includeCtor: false, completeDepth: true });
      let maxDepth = 0;
      relations.forEach((relationFunc) => {
        const {
          toFuncId, toFuncName, fromFuncId, depth, toFuncPointer,
        } = relationFunc;
        maxDepth = Math.max(depth.maxDepth, maxDepth);
        let node = {
          funcName: toFuncName,
          level: depth.maxDepth,
          type: toFuncId ? 'defined' : 'undefined',
        };
        if (toFuncPointer) {
          node.type = 'funcpointer';
        }
        if (this.reportData.invokedFlowData.nodes.has(toFuncName)) {
          node = this.reportData.invokedFlowData.nodes.get(toFuncName);
          node.level = node.level ? Math.max(node.level, depth.maxDepth) : depth.maxDepth;
        } else {
          this.reportData.invokedFlowData.nodes.set(toFuncName, node);
        }
        this.reportData.invokedFlowData.edges.add(`${this.versionFuncsMap.get(fromFuncId.toString()).functionName}|${toFuncName}`);
      });
      this.reportData.maxInvokeFuncDepth = Math.max(this.reportData.maxInvokeFuncDepth, maxDepth);
      if (maxDepth > 6) {
        this.reportData.invokeDepthGreaterThan6.push({
          funcName: func.functionName,
          depth: maxDepth,
        });
      }
    });
  }

  async handleFile(file) {
    await this.handleFileDataStream(file);
    await this.handleFunctions(file);
    if (this.sourceFileSuffixes.includes(file.extname)) {
      this.reportData.files.push({
        fileName: file.fileName,
        filePath: file.path,
        numberOfLines: file.numberOfLines,
        commentPercent: this.handlePercent(file.commentLines, file.numberOfLines),
      });
    }
  }

  getImageSize() {
    return {
      invokedFlowImage: {
        width: 600,
        height: 600,
      },
    };
  }

  /**
   * 数组去重和排序
   * @param {*} array
   * @param {*} key
   * @returns
   */
  arraySortAndDeduplication(array, key) {
    if (key) {
      return array.sort((a, b) => (a[key] >= b[key] ? 1 : -1));
    }
    return Array.from(new Set(array)).sort((a, b) => (a >= b ? 1 : -1));
  }

  /**
   * 生成静态分析xlsx表格
   * @param {*} fileIds
   * @param {Map} defectFileMap {lines: Set()}
   * @param {*} outputPath
   */
  async generateStaticAnalyzeReport(fileIds = [], defectFileLinesMap, outputPath) {
    await utility.arrayChunkOperation(fileIds, 30, async (fileId) => {
      const lines = this.arraySortAndDeduplication(defectFileLinesMap.get(fileId).lines);
      const fileContents = await files.getLinesOfRemoteFileStream(fileId, { start: lines[0], end: lines[lines.length - 1] });
      if (!fileContents || fileContents.length === 0) {
        return;
      }
      delete defectFileLinesMap.get(fileId).lines;
      fileContents.forEach((lineContent) => {
        if (!lines.includes(lineContent.line)) {
          return;
        }
        defectFileLinesMap.get(fileId)[lineContent.line] = lineContent.content;
      });
    });
    const excel = new Excels();
    excel.addSheet(path.basename(outputPath, '.xlsx'));
    const style = {
      alignment: {
        vertical: 'middle', horizontal: 'center',
      },
      font: {
        name: '宋体',
      },
    };
    const columns = [
      {
        header: '编号',
        key: 'index',
        style,
      },
      {
        header: '文件名',
        key: 'fileName',
        style,
      },
      {
        header: '代码行号',
        key: 'line',
        style,
      },
      {
        header: '违反代码',
        key: 'content',
        with: 400,
        style,
      },
      {
        header: '规则说明',
        key: 'ruleName',
        with: 400,
        style,
      },
      {
        header: '规则编号',
        key: 'ruleCode',
        with: 150,
        style,
      },
      {
        header: '测试人员',
        key: 'userName',
        style,
      },
      {
        header: '备注',
        key: 'description',
        style,
      },
    ];
    const data = [];
    let index = 1;
    this.reportData.rules.forEach((rule) => {
      if (rule.defects.length === 0) {
        return;
      }
      rule.defects.forEach((defect) => {
        if (!defectFileLinesMap.has(defect.fileId) || !defectFileLinesMap.get(defect.fileId)[defect.line]) {
          logger.info(`handle error of defect ${defect.ruleSetName}:${defect.ruleName}:${defect.id.toString()} `);
          return;
        }
        data.push({
          index,
          description: '',
          line: defect.line,
          ruleName: rule.ruleName,
          fileName: defect.fileName,
          userName: this.reportData.userName,
          ruleCode: `${rule.ruleSetName} ${rule.ruleCode}`,
          content: defectFileLinesMap.get(defect.fileId)[defect.line],
        });
        index += 1;
      });
    });
    excel.addRows(columns, data);
    excel.setRow(1, {
      font: {
        name: '宋体',
        size: 12,
        bold: true,
      },
    });
    [
      { position: 2, width: 30 },
      { position: 4, width: 50 },
      { position: 5, width: 50 },
      { position: 6, width: 30 },
    ].forEach(({ position, width }) => {
      excel.setColumn(position, { width });
    });

    return excel.writeFile(outputPath);
  }

  /**
  * 单文件复制静态资源
  * @param {Object} data 数据
  * @param {string} output 文件输出路径
  * @returns
  */
  generateSummaryReport(data, output) {
    const template = 'file-template';
    return this.generateReport(template, data, output);
  }

  async handleStaticAnalyze(output) {
    const defectList = await defects.getDefects(this.versionId, { ...this.filters, status: this.filters.defectStatus });
    const defectFileLinesMap = new Map();
    const fileIds = new Set();
    const rulesSetMap = new Map();
    this.reportData.defectCount = defectList.length;
    defectList.forEach((defect) => {
      const ruleId = defect.ruleId.toString();
      const fileId = defect.fileId.toString();
      const {
        ruleSetName, ruleName, ruleCode, ruleOrderIndex, filePath, locations,
      } = defect;
      if (!rulesSetMap.has(ruleSetName)) {
        rulesSetMap.set(ruleSetName, new Map());
      }
      if (!rulesSetMap.get(ruleSetName).has(ruleId)) {
        rulesSetMap.get(ruleSetName).set(ruleId, {
          ruleSetName,
          ruleName,
          ruleCode,
          ruleOrderIndex,
          defects: [],
        });
      }
      rulesSetMap.get(ruleSetName).get(ruleId).defects.push({
        fileId,
        content: '',
        id: defect._id,
        line: locations.begin.line,
        fileName: path.basename(filePath),
      });
      if (!defectFileLinesMap.has(fileId)) {
        defectFileLinesMap.set(fileId, { lines: [] });
      }
      defectFileLinesMap.get(fileId).lines.push(locations.begin.line);
      fileIds.add(fileId);
    });
    const keys = Array.from(rulesSetMap.keys()).sort();
    keys.forEach((ruleSetName) => {
      this.reportData.rules.push(...rulesSetMap.get(ruleSetName).values());
    });
    await this.generateStaticAnalyzeReport(Array.from(fileIds), defectFileLinesMap, output);
  }

  /**
 *  mermaid-cli 生成图片
 * @param {String} inputPath  输入xxx.md格式的图表文件，具体格式npx mmdc -h
 * @param {String} outputPath 输出路径
 * @param {String} fileName 输出文件名
 * @param {String} extname  输出的文件扩展名
 * @param {String} key  标记图片的key
 */
  async generateFlowImage(inputFilePath, outputPath, fileName, extname, key) {
    const mmdcpath = `"${path.join(__dirname, '../', '../', '../', '../', '../', 'node_modules', '.bin', 'mmdc')}"`;
    const puppeteerConfig = `"${path.join(__dirname, 'puppeteer-config.json')}"`;
    const mermaidConfig = `"${path.join(__dirname, 'mermaid-config.json')}"`;
    await new Promise((resolve, reject) => {
      const mmdc = spawn(mmdcpath, [
        '-i', inputFilePath,
        '-o', path.join(outputPath, `${fileName}.${extname}`),
        '-p', puppeteerConfig,
        '-c', mermaidConfig,
        '-w', 4000,
        '-H', 3000,
        '-s', 2,
      ], { shell: true });
      mmdc.stderr.on('data', (err) => {
        logger.info(`mmdc stderr: ${err}`);
        reject(err);
      });
      mmdc.on('error', (error) => {
        logger.info(`mmdc error: ${error}`);
        reject(error);
      });
      mmdc.on('close', (code) => {
        logger.info(`mmdc generate ${key} ${extname}:${code === 0 ? 'success' : 'failed'}`);
        resolve();
      });
    });
    // rename output filename
    await new Promise((resolve, reject) => {
      const oldFileName = path.join(outputPath, `${fileName}-1.${extname}`);
      const newFileName = path.join(outputPath, `${fileName}.${extname}`);
      fs.rename(oldFileName, newFileName, (err) => {
        if (err) {
          reject(new Error(`rename file name:${oldFileName} error:${err}`));
        }
        resolve();
      });
    });
    // delete input xxxx.md
    await new Promise((resolve, reject) => {
      fs.unlink(inputFilePath, (err) => {
        if (err) {
          reject(new Error(`delete input ${inputFilePath} error:${err}`));
        }
        resolve();
      });
    });
  }

  async generateFuncsFlowChart() {
    const nodeNameToNumberMap = new Map();
    const topNode = {
      funcName: '0',
      number: 1,
      type: '',
      level: 0,
    };
    const flowChartData = {
      nodes: [topNode],
      edges: [],
    };
    nodeNameToNumberMap.set(topNode.funcName, topNode.number);
    const { nodes, edges } = this.reportData.invokedFlowData;
    if (!nodes.size || !edges.size) {
      return;
    }
    nodes.forEach((value,) => {
      if (value.fanIn === 0) {
        value.level = 1;
        edges.add(`0|${value.funcName}`);
      }
      flowChartData.nodes.push(value);
    });
    // 函数节点排序
    flowChartData.nodes.sort((a, b) => a.level - b.level);
    this.reportData.invokedFlowData.nodes = [];
    delete this.reportData.invokedFlowData.edges;
    // 对节点进行层级分组
    const newNodes = [];
    flowChartData.nodes.forEach((node, index) => {
      if (!node.number) {
        node.number = index + 1;
        nodeNameToNumberMap.set(node.funcName, node.number);
      }
      if (node.level !== 0) {
        let type = '';
        switch (node.type) {
          case 'defined': {
            type = '已定义';
            break;
          }
          case 'undefined': {
            type = '未定义';
            break;
          }
          case 'funcpointer': {
            type = '函数指针';
            break;
          }
          default: break;
        }
        this.reportData.invokedFlowData.nodes.push({ ...node, type });
      }
      if (!newNodes[node.level]) {
        newNodes[node.level] = [];
      }
      newNodes[node.level].push(node);
    });
    flowChartData.nodes = newNodes;
    edges.forEach((value) => {
      const [fromFuncName, toFuncName] = value.split('|');
      const fromNodeNumber = nodeNameToNumberMap.get(fromFuncName);
      const toNodeNumber = nodeNameToNumberMap.get(toFuncName);
      let line = '---';
      if (fromFuncName === '0') {
        flowChartData.edges.push({ from: fromNodeNumber, to: toNodeNumber, line });
        return;
      }
      if (!nodes.has(fromFuncName) || !nodes.has(toFuncName)) {
        return;
      }
      // 下层函数调用上层函数使用虚线
      if (nodes.get(fromFuncName).level > nodes.get(toFuncName)) {
        line = '-.-';
      }
      flowChartData.edges.push({ from: fromNodeNumber, to: toNodeNumber, line });
    });
    const versionId = this.versionId.toString();
    const outputPath = path.join(this.outputDir, versionId);
    const mdFilePath = `${outputPath}.md`;
    const imageFilePath = `${outputPath}.png`;
    const markdownFileData = await utility.getDotTemplate(path.join(__dirname, 'template', 'dot', 'function-flow-chart-template.md.dot'), flowChartData);
    await utility.writeFile(markdownFileData, mdFilePath);
    await this.generateFlowImage(mdFilePath, this.outputDir, versionId, 'png', `${this.project.projectName}:${this.version.versionName}`);
    this.reportData.invokedFlowImage = imageFilePath;
  }
}
module.exports = DefaultReporter;
